﻿using System.Collections.Generic;
using CryptoQrUtilities.Cryption;
using System.Text;
using System.Drawing;
using System.IO;
using System.Xml;
using System;

namespace CryptoQrUtilities
{
    using System.Runtime.Serialization;

    // For backward compatibility of 'QrMessage's and as long as any new such message type inherits its predecessor,
    // the 'LatestMessageType' defines the type of the most recent defined message type:
    using LatestMessageType = MultiKeyQrMessage;

    /// <summary>
    /// A message type carrying a QR bitmap and can be defined as encrypted.
    /// </summary>
    [DataContract]
    public class QrMessage
    {
        /// <summary>
        /// THIS MUST NEVER CHANGE for backward compatibility.
        /// Used as a key in the <see cref="SDecryptionMethods"/> map.
        /// </summary>
        private const string c_messageTypeVersion = "1.0.0.0";

        /// <summary>
        /// Every <see cref="QrMessage"/> type adds here a method for decrypting its encrypted envelope.
        /// </summary>
        protected static readonly Dictionary<string, IMessageDencrypter> SDecryptionMethods = new Dictionary<string, IMessageDencrypter>();

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="CryptoQrUtilities.QrMessage"/> class for encrypted or simple plain text.
        /// This mesage type handles version 1.0.0.0 of the QR messages, which can handle one public RSA key for the AES key.
        /// </summary>
        /// <param name="message">The message.</param>
        /// <param name="useEncryption">Whether to encrypt the given message.</param>
        public QrMessage(string message, bool useEncryption = false)
        {
            IsEncrypted = useEncryption;

            if (IsEncrypted)
            {
                if (!SDecryptionMethods.ContainsKey(c_messageTypeVersion))
                {
                    throw new InvalidOperationException(
                        "'QrMessage' crypted abilities needs the .NET RSA Container set. Use 'QrMessage.AddDotNetContainerDecryptionMethod()'");
                }

                var decryptor = SDecryptionMethods[c_messageTypeVersion] as SingleKeyDencryptor;

                if (decryptor == null)
                {
                    throw new InvalidCastException("'QrMessage' is expecting a 'SingleKeyDencryptor' decryptor set.");
                }

                var envelope = new CryptedEnvelope(message, new List<IRsaProvider> { decryptor.RsaProvider });

                MessageContents = envelope.EncryptedContents;
            }
            else
            {
                MessageContents = Encoding.Unicode.GetBytes(message);
            }

            Version = c_messageTypeVersion;
        }

        /// <summary>
        /// Creates a <see cref="QrMessage"/> instance empty for leaving the initialization to derived classes.
        /// </summary>
        protected QrMessage()
        {
            IsEncrypted = false;
        }

        #endregion

        #region Aids for setting decryption methods for backward compatibility

        /// <summary>
        /// Add the ability to decrypt using a .NET container for RSA private keys.
        /// Supports the version '1.0.0.0' of the <see cref="QrMessage"/>.
        /// </summary>
        /// <param name="rsaKeyContainerName">Name of the .NET container where the RSA private key can be found.</param>
        /// <param name="useOaepPadding">Whether to use OAEP padding for the RSA algorithm.</param>
        /// <param name="useMachineLevelStore">Whether the scope of the created container is the machine or the current user.</param>
        /// <param name="rsaKeySize">The size in bits of the keys for the created RSA container.</param>
        public static void AddDotNetContainerDecryptionMethod(
            string rsaKeyContainerName,
            bool useOaepPadding = true,
            bool useMachineLevelStore = true,
            int rsaKeySize = 1024)
        {
            ContainerBasedRsaProvider.CreateRsaPersistentKeyContainer(rsaKeyContainerName, useMachineLevelStore, rsaKeySize);
            SDecryptionMethods.Add(
                c_messageTypeVersion,
                new SingleKeyDencryptor(new ContainerBasedRsaProvider(rsaKeyContainerName, useOaepPadding)));
        }

        #endregion

        /// <summary>
        /// Deserializes the <see cref="LatestMessageType"/> from a given QR bitmap.
        /// </summary>
        /// <param name="qr">QR bitmap.</param>
        public static LatestMessageType DeserializeFromQr(Bitmap qr)
        {
            try
            {
                var qrUtils = new QrUtilitiesWrapper();
                var decodedQr = qrUtils.GetQrDecoding(qr);

                if (decodedQr == null || decodedQr.Text == null)
                {
                    throw new InvalidOperationException("Given QR could not be decoded.");
                }

                return DeserializeEncodedMessage(decodedQr.Text);
            }
            catch (Exception ex)
            {
                Logger.LogFatal("While trying to deserialize a QR bitmap, caught exception {0}", ex.ToString());

                throw;
            }
        }

        /// <summary>
        /// Deserializes the <see cref="LatestMessageType"/> from a given encoded string message.
        /// </summary>
        /// <param name="encodedMessage">Base64 encoded message for decoding.</param>
        public static LatestMessageType DeserializeEncodedMessage(string encodedMessage)
        {
            try
            {
                var serializer = new DataContractSerializer(typeof(LatestMessageType));
                using (var reader = XmlDictionaryReader.CreateBinaryReader(encodedMessage.FromBase64(), XmlDictionaryReaderQuotas.Max))
                {
                    return (LatestMessageType)serializer.ReadObject(reader);
                }
            }
            catch (Exception ex)
            {
                Logger.LogFatal("While trying to deserialize an encoded message, caught exception {0}", ex.ToString());

                throw;
            }
        }

        /// <summary>
        /// Serialize this instance contents into a QR bitmap.
        /// </summary>
        public Bitmap SerializeIntoQr()
        {
            try
            {
                var serializer = new DataContractSerializer(GetType());
                var qrUtils = new QrUtilitiesWrapper();

                using (var binaryStream = new MemoryStream())
                {
                    using (var writer = XmlDictionaryWriter.CreateBinaryWriter(binaryStream))
                    {
                        serializer.WriteObject(writer, this);
                    }

                    return qrUtils.GetQrEncoding(binaryStream.ToArray().ToBase64());
                }
            }
            catch (Exception ex)
            {
                Logger.LogFatal("While trying to Serialize text into QR, caught exception {0}", ex.ToString());

                throw;
            }
        }

        /// <summary>
        /// If needed, decrypts the byte buffer of <see cref="MessageContents"/> and returns the content message.
        /// This method identifies the real type of the <see cref="QrMessage"/> for the translation operation.
        /// </summary>
        /// <returns>The contents of the message within this object.</returns>
        public string GetMessageClearText()
        {
            if (!IsEncrypted)
            {
                return Encoding.Unicode.GetString(MessageContents);
            }

            // Decrypt the encrypted message:
            if (!SDecryptionMethods.ContainsKey(Version))
            {
                throw new InvalidDataException(string.Format("There's no decryption method set for QR message version '{0}'", Version));
            }

            var decryptor = SDecryptionMethods[Version];

            return decryptor.DecryptMessage(this);
        }

        /// <summary>
        /// Gets the version of the framework supporting this message structure. May be used for backward/forward compatibility or compatibility check.
        /// </summary>
        [DataMember]
        public string Version { get; protected set; }

        /// <summary>
        /// Gets the message contents. This is a textual message even when encrypted.
        /// </summary>
        [DataMember]
        public byte[] MessageContents { get; protected set; }

        /// <summary>
        /// Gets a value indicating whether this instance is encryped.
        /// </summary>
        [DataMember]
        public bool IsEncrypted { get; protected set; }
    }
}
